package com.webex.ta.hydra.plugin;

import com.webex.ta.hydra.core.KeywordRegistry;
import com.webex.ta.hydra.core.PropertyManager;
import com.webex.ta.hydra.util.HydraUtils;
import com.webex.ta.hydra.util.MiscUtils;
import com.webex.ta.hydra.util.ProgressObserver;
import com.webex.ta.hydra.gui.action.ActionRouter;
import org.dom4j.*;
import org.dom4j.io.SAXReader;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
import java.util.concurrent.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

/**
 * Created by Cisco WebEx.
 * User: vegaz
 * Date: 2010-9-28
 * Time: 10:23:43
 */
public class PluginManager {
    private static PluginManager instance;

    // installed
    private HashMap<String, PluginJar> pluginJars = new HashMap<String, PluginJar>();
    private HashMap<String, PluginJar> installedPlugins = new HashMap<String, PluginJar>(10);

    // available
    private Map<String, Plugin> availablePlugins = new HashMap<String, Plugin>(20);
    private List<PluginSet> pluginSets = new ArrayList<PluginSet>(10);

    private Future<Integer> downloadTask, unzipTask;
    private ExecutorService executor = Executors.newSingleThreadExecutor();

    private static final String DISABLE_PFX = "plugin-disable.";

    private PluginManager() {
    }

    public static PluginManager getInstance() {
        if (instance == null)
            instance = new PluginManager();
        return instance;
    }

    private void _loadPlugin(File pluginDir) {
        File[] jars = pluginDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".jar");
            }
        });

        String pluginName = null;

        for (File jar : jars) {
            try {
                PluginJar pj = new PluginJar(jar);
                pluginJars.put(jar.getAbsolutePath(), pj);
                if (pj.getPlugin() != null) {
                    pluginName = pj.getPlugin().getName();
                    installedPlugins.put(pluginName, pj);
                }
            } catch (Exception ignore) {
                ignore.printStackTrace();
            }
        }

        if (pluginName != null) {
            boolean disabled = Boolean.valueOf(
                    PropertyManager.getInstance().getProperty(DISABLE_PFX + pluginName, "false"));
            if (!disabled)
                _activatePlugin(pluginDir);
        }
    }

    public void initInstalledPlugins() {
        File pluginPath = new File(HydraUtils.getHydraPluginPath());
        File[] pluginDirs = pluginPath.listFiles(new FileFilter() {
            public boolean accept(File file) {
                return file.isDirectory();
            }
        });

        if (pluginDirs != null) {
            for (File dir : pluginDirs) {
                _loadPlugin(dir);
                ActionRouter.getInstance().populateCommandMap(dir);
            }
            KeywordRegistry.getInstance().loadPluginKeywords();
        }
    }

    private void _activatePlugin(File pluginDir) {
        File[] jars = pluginDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".jar");
            }
        });

        if (jars == null) return;

        try {
            List<PluginJar> delayed = new ArrayList<PluginJar>();
            for (File jar : jars) {
                PluginJar pj = pluginJars.get(jar.getAbsolutePath());
                if (pj == null) {
                    // if hasn't been loaded, load it first
                    pj = new PluginJar(jar);
                    pluginJars.put(jar.getAbsolutePath(), pj);
                    if (pj.getPlugin() != null) {
                        installedPlugins.put(pj.getPlugin().getName(), pj);
                    }
                }
                if (pj.getPlugin() != null) {
                    delayed.add(pj);
                } else {
                    pj.activate();
                }
            }

            for (PluginJar delayedJar : delayed) {
                delayedJar.activate();
            }
        } catch (Exception ignore) {
            ignore.printStackTrace();
        }
    }

    public boolean activate(String pluginName) {
        File pluginDir = new File(HydraUtils.getHydraPluginPath(), pluginName);
        _activatePlugin(pluginDir);
        PropertyManager.getInstance().setUserProperty(DISABLE_PFX + pluginName, "false");
        KeywordRegistry.getInstance().loadPluginKeywords();
        ActionRouter.getInstance().populatePluginCommandMap(pluginName);
        return true;
    }

    public boolean deactivate(String pluginName) {
        boolean result = _deactivate(pluginName);
        PropertyManager.getInstance().setUserProperty(DISABLE_PFX + pluginName, "true");
        KeywordRegistry.getInstance().loadPluginKeywords();
        return result;
    }

    private boolean isDeactivated(String pluginName) {
        return Boolean.valueOf(
                PropertyManager.getInstance().getProperty(DISABLE_PFX + pluginName, "false"));
    }

    public Plugin[] getActivatedPlugins() {
        List<Plugin> activated = new ArrayList<Plugin>(5);
        for (PluginJar pj : installedPlugins.values()) {
            String pluginName = pj.getPlugin().getName();
            if (!isDeactivated(pluginName))
                activated.add(pj.getPlugin());
        }
        return activated.toArray(new Plugin[activated.size()]);
    }

    public Plugin[] getInstalledPlugins() {
        Collection<PluginJar> plugins = installedPlugins.values();
        Plugin[] result = new Plugin[plugins.size()];
        int i = 0;
        for (PluginJar pj : plugins) {
            result[i] = pj.getPlugin();
            i++;
        }
        return result;
    }

    public Plugin[] getAvailablePlugins() {
        return availablePlugins.values().toArray(new Plugin[availablePlugins.size()]);
    }


    private static final String PLUGIN_SET = "PLUGIN_SET";

    public void downloadPluginList() throws Exception {
        SAXReader reader = new SAXReader();
        // TODO hard coded
//        Document doc = reader.read(new URL("file:///F:\\HYDRA_PLUGINS.xml"));
        Document doc = reader.read(new URL("http://ta.webex.com.cn/HYDRA_PLUGINS.xml"));
        Element root = doc.getRootElement();
        pluginSets.clear();
        for (Iterator pluginSets = root.elementIterator(PLUGIN_SET); pluginSets.hasNext();) {
            Element psNode = (Element) pluginSets.next();
            PluginSet ps = new PluginSet(psNode.attributeValue("NAME"));
            this.pluginSets.add(ps);
            for (Iterator pluginSetEntries = psNode.elementIterator("PLUGIN_SET_ENTRY");
                 pluginSetEntries.hasNext();) {
                Element psEntry = (Element) pluginSetEntries.next();
                ps.addPlugin(psEntry.getText());
            }
        }

        for (Iterator plugins = root.elementIterator("PLUGIN"); plugins.hasNext();) {
            Element pNode = (Element) plugins.next();
            Plugin plugin = new Plugin();
            plugin.setName(pNode.attributeValue("NAME"));
            plugin.setAuthor(pNode.element("AUTHOR").getText());
            plugin.setDescription(pNode.element("DESCRIPTION").getText());
            availablePlugins.put(plugin.getName(), plugin);
            for (Iterator branches = pNode.elementIterator("BRANCH"); branches.hasNext();) {
                Element bNode = (Element) branches.next();
                Plugin.Branch branch = new Plugin.Branch();
                branch.setDate(bNode.attributeValue("DATE"));
                branch.setVersion(bNode.attributeValue("VERSION"));
                branch.setSize(Integer.valueOf(bNode.element("DOWNLOAD").attributeValue("SIZE")));
                branch.setDownload(bNode.element("DOWNLOAD").getText());
                plugin.addBranch(branch);
            }
        }

        // set category
        for (PluginSet ps : pluginSets) {
            for (String pluginName : ps.plugins) {
                if (availablePlugins.containsKey(pluginName)) {
                    availablePlugins.get(pluginName).setCategory(ps.getName());
                }
            }
        }

        // filter installed plugins
//        for (PluginJar pj : this.installedPlugins) {
//            String pluginName = pj.getPlugin().getName();
//            for (int i = availablePlugins.size() - 1; i >=0; i--) {
//                Plugin plugin = availablePlugins.get(i);
//                if (plugin.getName().equalsIgnoreCase(pluginName)) {
//                    this.availablePlugins.remove(i);
//                }
//            }
//        }
    }

    public boolean canUpdate(Plugin plugin) {
        try {
            return MiscUtils.compareStrings(availablePlugins.get(plugin.getName()).getLatestVersion(),
                    plugin.getVersion(), true) > 0;
        } catch (Exception e) {
            return false;
        }
    }

    public boolean canUpdate(String pluginName) {
        for (PluginJar pj : installedPlugins.values()) {
            if (pj.getPlugin().getName().equals(pluginName)) {
                return canUpdate(pj.getPlugin());
            }
        }
        return false;
    }

//    public boolean activate(String plugin) {
//        File pluginFolder = new File(HydraUtils.getHydraPluginPath(), plugin);
//        if (!pluginFolder.exists() || !pluginFolder.isDirectory())
//            return false;
//
//        try {
//            for (File f : pluginFolder.listFiles()) {
//                PluginJar pj = new PluginJar(f);
//                pluginJars.put(f.getAbsolutePath(), pj);
//                if (pj.getPlugin() != null) {
//                    installedPlugins.add(pj);
//                }
//                KeywordRegistry.getInstance().loadPluginKeywords();
//            }
//        } catch (IOException e) {
//            e.printStackTrace();
//            return false;
//        }
//        return true;
//    }


    public boolean uninstall(String plugin) {
        if (_deactivate(plugin)) {
            installedPlugins.remove(plugin);
            _uninstall(plugin);
            return true;
        } else {
            return false;
        }
    }

    private boolean _deactivate(String plugin) {
        File pluginFolder = new File(HydraUtils.getHydraPluginPath(), plugin);
        if (!pluginFolder.exists() || !pluginFolder.isDirectory())
            return false;

        try {
            for (File f : pluginFolder.listFiles()) {
                PluginJar pj = pluginJars.get(f.getAbsolutePath());
                pj.deactivate();
                pluginJars.remove(f.getAbsolutePath());
//                pluginJars.remove(f.getAbsolutePath());
            }
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * remove the plugin folder
     *
     * @param pluginName
     */
    private void _uninstall(String pluginName) {
        File pluginFolder = new File(HydraUtils.getHydraPluginPath(), pluginName);
        if (pluginFolder.exists()) {
            for (File f : pluginFolder.listFiles()) {
                if (f.isDirectory())
                    MiscUtils.deleteDirectory(f);
                else
                    f.delete();
            }
        }
    }

    public boolean cancelInstall() {
        if (downloadTask != null && downloadTask.cancel(true))
            return true;

        if (unzipTask != null && unzipTask.cancel(true))
            return true;

        return false;
    }

    public boolean install(String pluginName, ProgressObserver obsever) {
        boolean success = _install(pluginName, obsever);
        if (!success) {
            // clean up
            _uninstall(pluginName);
        }
        activate(pluginName);
        KeywordRegistry.getInstance().loadPluginKeywords();
        return success;
    }

    private boolean _install(String pluginName, ProgressObserver observer) {
        Plugin plugin = availablePlugins.get(pluginName);
        if (plugin == null)
            return false;

        Plugin.Branch branch = plugin.getLatestBranch();
        if (branch == null)
            return false;

        String url = branch.getDownload();
        File tmpDir = new File(HydraUtils.getHydraTempPath());
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }
        String tmpFile = new File(HydraUtils.getHydraTempPath(), pluginName + ".zip").getAbsolutePath();

        downloadTask = executor.submit(
                new DownloadTask(url, tmpFile, observer));
        Integer result;
        try {
            result = downloadTask.get();
        } catch (Exception e) {
            return false;
        }
        if (result < 0)
            return false;
//        downloadTask = null;

        unzipTask = executor.submit(
                new UnzipTask(tmpFile, HydraUtils.getHydraPluginPath() + File.separator + pluginName, observer));
        try {
            result = unzipTask.get();
        } catch (Exception e) {
            return false;
        }
        if (result < 0)
            return false;

        return true;
    }

    private static class DownloadTask implements Callable<Integer> {
        private String url;
        private String dest;
        private ProgressObserver observer;

        public DownloadTask(String url, String dest, ProgressObserver observer) {
            this.url = url;
            this.dest = dest;
            this.observer = observer;
        }

        public Integer call() throws Exception {
            InputStream in = null;
            OutputStream out = null;
            try {
                URLConnection conn = new URL(url).openConnection();
                if (observer != null)
                    observer.setMaximum(conn.getContentLength());
                in = conn.getInputStream();
                out = new FileOutputStream(dest);
                HydraUtils.copyStream(observer, in, out, true);
            } catch (IOException ignore) {
                return -1;
            } finally {
                HydraUtils.closeQuietly(in);
                HydraUtils.closeQuietly(out);
            }
            return 0;
        }
    }

    private static class UnzipTask implements Callable<Integer> {
        private String src;
        private String dest;
        private ProgressObserver observer;

        public UnzipTask(String src, String dest, ProgressObserver observer) {
            this.src = src;
            this.dest = dest;
            this.observer = observer;
        }

        public Integer call() throws Exception {
            if (!dest.endsWith(File.separator))
                dest = dest.concat(File.separator);

            File pluginDir = new File(dest);
            if (!pluginDir.exists())
                pluginDir.mkdirs();

            ZipInputStream zin = new ZipInputStream(new FileInputStream(src));
            ZipEntry entry;
            ZipFile zip = null;
            try {
                zip = new ZipFile(src);
                if (observer != null)
                    observer.setMaximum(zip.size());
                int counter = 0;
                while ((entry = zin.getNextEntry()) != null) {
                    if (Thread.interrupted())
                        return -1;
                    File zFile = new File(dest + entry.getName());
                    File fPath = new File(zFile.getParentFile().getParent());
                    if (entry.isDirectory()) {
                        if (!zFile.exists())
                            zFile.mkdirs();
                        zin.closeEntry();
                    } else {
                        if (!fPath.exists())
                            fPath.mkdirs();
                        FileOutputStream fo = new FileOutputStream(zFile);
                        int i;
                        byte buffer[] = new byte[256];
                        while ((i = zin.read(buffer)) != -1)
                            fo.write(buffer, 0, i);
                        zin.closeEntry();
                        fo.close();
                    }
                    counter++;
                    if (observer != null) {
                        observer.setStatus("Extracting " + zFile.getName());
                        observer.setValue(counter);
                    }
                }
            } catch (IOException e) {
                return -1;
            } finally {
                HydraUtils.closeQuietly(zin);
                zip.close();
            }
            return 0;
        }
    }
}
